combit List & Label 30 - .NET Help
Programming Introduction / Examples / General / Using the Repository-Mode
Using the Repository-Mode

With the help of Repositories most of the file accesses could be redirected to an self-defined virtual file system. This allows you to manage all project files and their related files in a database. The implementation of such strategy is described in the following setps. An Overview of the use and functionality of the Repository could be found in namespace combit.Reporting.Repository. Following will be demonstrated how the Repository can be used in List & Label. The supplied programming samples for ASP.NET and WinForms show the usage in detail.

 

Tip

In the following, code sections are shown which have been reduced to a minimum due to their clarity. Any auxiliary functions shown, but can be viewed in full in the supplied samples, since the following description is based on the implementation of the IRepository interface using an SQLite database and its subsequent use in practice in detail in the examples.

 

 Step 1: Definition of the data retention
The managing of the files in List & Label like project files for the designer, P-files for printer settings, table of contents, drawings, shapefiles etc. are normally controlled by the file system of the operating system. Thus List & Label works on the basis of file names and its paths.

As soon as you decide to use the Repository, you need to deal with that question and find an suitable "storage", because of List & Label does no longer work with file paths rather only with so called Repository-IDs. With the help of these IDs the requested elements could be asked and used. Numerous applications use for their data storage something like databases where SQL databases is often chosen. That is the reason why we will use for simple demonstration an SQLite database. And the namespace System.Data.SQLite from the .NET Framework is used for:

public class SQLiteFileRepository
{
    private readonly IDbConnection _db;
    public SQLiteFileRepository(string databasePath)
    {
        bool needsDatabaseInit = !File.Exists(databasePath);
        _db = new SQLiteConnection("Data Source=" + databasePath);
        _db.Open();
        if (needsDatabaseInit)
            DropAndCreateTables();
    }
   
    private void DropAndCreateTables()
    {
        _db.CreateCommand(@"
            DROP TABLE IF EXISTS RepoItems;
            CREATE TABLE IF NOT EXISTS RepoItems (
                ItemID               TEXT,
                Type                 TEXT,
                Descriptor           TEXT,
                TimestampUTC         INT,
                FileContent          BLOB
            );").ExecuteNonQuery();
    }
}
 Step 2: Necessary implementation of the interface IRepository

Now the implementation of the interface IRepository need to be done, so you can react individual on the requests of List & Label:        

public class SQLiteFileRepository : IRepository
{
    public bool ContainsItem(string itemID)
    {
        // ...
    }
    public void CreateOrUpdateItem(RepositoryItem item, string userImportData, Stream sourceStream)
    {
        // ...
    }
    public void DeleteItem(string itemID)
    {
        // ...
    }
    public IEnumerable<RepositoryItem> GetAllItems()
    {
        // ...
    }
    public RepositoryItem GetItem(string itemID)
    {
        // ...
    }
    public void LoadItem(string itemID, Stream destinationStream, CancellationToken cancelToken)
    {
        // ...
    }
    public bool LockItem(string id)
    {
        // ...
    }
    public void UnlockItem(string id)
    {
        // ...
    }
}
 IRepository.ContainsItem

According to the description in IRepository.ContainsItem this function is called to check if an suitable element exists in the Repository. Therefore you need to ask the database if the requested ID exists:

public class SQLiteFileRepository : IRepository
{
    // ...
   
    public bool ContainsItem(string itemID)
    {
        int result = Convert.ToInt32(_db.CreateCommand(
            "SELECT COUNT(*) FROM RepoItems WHERE ItemID = @ItemID")
               .SetParameter("ItemID", itemID).ExecuteScalar());
        return (result == 1);
    }
   
    // ...

}
 IRepository.CreateOrUpdateItem

According to the description in IRepository.CreateOrUpdateItem this function is called, if a new element will added to the Repository or an existing one should be updated. However this function is also called, if only the meta data of the element should be updated or the meta data should be added independently of the content:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void CreateOrUpdateItem(RepositoryItem item, string userImportData, Stream sourceStream)
    {
        // Convert stream from List & Label to byte array to store it in the DB
        // Warning: sourceStream may be null! In that case, only the metadata should be changed in the database.
        byte[] fileContent = null;
        bool setMetadataOnly;
        if (sourceStream != null)
        {
            using (var memStream = new MemoryStream())
            {
                sourceStream.CopyTo(memStream);
                fileContent = memStream.ToArray();
            }
            setMetadataOnly = false;
        }
        else
        {
            setMetadataOnly = true;
        }

        // Need to update an existing item or is it a new one?
        RepositoryItem itemToInsert;
        bool isUpdate = ContainsItem(item.InternalID);
        if (isUpdate)   
        {
            // Update an existing Repository Item
            itemToInsert = GetItemsFromDb(item.InternalID).First();
            itemToInsert.Descriptor = item.Descriptor;
            itemToInsert.LastModificationUTC = item.LastModificationUTC;
        }
        else  
        {
            // Add new Repository Item
            itemToInsert = new RepositoryItem(item.InternalID, item.Descriptor, item.Type, item.LastModificationUTC);
        }
       
        // Create a suitable SQL query for INSERT / UPDATE and call it with/without the file content.
        string sqlQuery;
        if (isUpdate)  // UPDATE
        {
            if (setMetadataOnly)
            {
                sqlQuery = @"UPDATE RepoItems
                             SET Descriptor = @Descriptor, TimestampUTC = @TimestampUTC
                             WHERE ItemID = @ItemID";
            }
            else
            {
                sqlQuery = @"UPDATE RepoItems
                             SET Descriptor = @Descriptor, TimestampUTC = @TimestampUTC, FileContent = @FileContent
                             WHERE ItemID = @ItemID";
            }
        }
        else    // INSERT
        {
            if (setMetadataOnly)
            {
                sqlQuery = @"INSERT INTO RepoItems (ItemID,  Type,  Descriptor,  TimestampUTC)
                                          VALUES  (@ItemID, @Type, @Descriptor, @TimestampUTC)";
            }
            else
            {
                sqlQuery = @"INSERT INTO RepoItems (ItemID,  Type,  Descriptor,  TimestampUTC,  FileContent)
                                          VALUES  (@ItemID, @Type, @Descriptor, @TimestampUTC, @FileContent)";
            }
        }
        _db.CreateCommand(sqlQuery)
            .SetParameter("ItemID", itemToInsert.InternalID)
            .SetParameter("Type", itemToInsert.Type)
            .SetParameter("Descriptor", itemToInsert.Descriptor)
            .SetParameter("FileContent", fileContent)
            .SetParameter("TimestampUTC", itemToInsert.LastModificationUTC.ToBinary()) // Note that this is always UTC time (convert to local time for the UI)
            .ExecuteNonQuery();
    }
    // ...
}
 IRepository.DeleteItem

According to the description in IRepository.DeleteItem this function is called, if an element should be deleted from the Repository. Therefore the related record have to be deleted from the SQLite database:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void DeleteItem(string itemID)
    {
        _db.CreateCommand("DELETE FROM RepoItems WHERE ItemID = @ItemID")
            .SetParameter("ItemID", itemID)
            .ExecuteNonQuery();
    }
    // ...
}
 IRepository.GetAllItems

This implementation is called to get all existing elements in the Repository (see also IRepository.GetAllItems):

public class SQLiteFileRepository : IRepository
{
    // ...
    public IEnumerable<RepositoryItem> GetAllItems()
    {
        List<RepositoryItem> result = new List<RepositoryItem>();
        var cmd = _db.CreateCommand("SELECT ItemID, Type, Descriptor, TimestampUTC, LENGTH(FileContent) FROM RepoItems");
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                result.Add(new RepositoryItem(
                    /* ItemID */ reader.GetString(0),
                    /* Descriptor */ reader.GetString(2),
                    /* Type */ reader.GetString(1),
                    /* TimestampUTC */ DateTime.FromBinary(reader.GetInt64(3)))
                {
                    IsEmpty = reader.IsDBNull(4) ? true : (reader.GetInt32(4) == 0 ? true : false)
                });
            }
        }
        return result;
    }
    // ...
}
 IRepository.GetItem

To return only one element from the Repository the implementation of IRepository.GetItem is called with the requested ID:

public class SQLiteFileRepository : IRepository
{
    // ...
    public RepositoryItem GetItem()
    {
        List<RepositoryItem> result = new List<RepositoryItem>();
        var cmd = _db.CreateCommand("SELECT ItemID, Type, Descriptor, TimestampUTC, LENGTH(FileContent) FROM RepoItems WHERE ItemID = @ItemId");
        cmd.SetParameter("ItemId", itemId);
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                result.Add(new RepositoryItem(
                    /* ItemID */ reader.GetString(0),
                    /* Descriptor */ reader.GetString(2),
                    /* Type */ reader.GetString(1),
                    /* TimestampUTC */ DateTime.FromBinary(reader.GetInt64(3)))
                {
                    IsEmpty = reader.IsDBNull(4) ? true : (reader.GetInt32(4) == 0 ? true : false)
                });
            }
        }
        return result.FirstOrDefault();
    }
    // ...
}
 IRepository.LoadItem

If the content of an element is really requested and should be loaded from the Repository, for example to open a project file in the designer, the implementation of IRepository.LoadItem is called and we have to return the content of the requested element as stream:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void LoadItem(string itemID, Stream destinationStream, CancellationToken cancelToken)
    {
        byte[] content = (byte[])_db.CreateCommand(
             "SELECT FileContent FROM RepoItems WHERE ItemID = @ItemID")
                .SetParameter("ItemID", itemID).ExecuteScalar();
        destinationStream.Write(content, 0, content.Length);
    }
    // ...
}
 IRepository.LockItem

If the requirement exists to editing an element exclusively - for example if a project is opening in the designer - an exclusively access with the help of IRepository.LockItem could be implemented to lock the element:

public class SQLiteFileRepository : IRepository
{
    // ...
    public bool LockItem(string id)
    {
        // IMPORTANT: Always implement a fallback to release the locks (e.g. timeout).
        // Especially when used in a network application, UnlockItem() might not get called due to network problems.

        // Return true, if the lock was acquired or if no locking is implemented.
        // Return false, if the item is locked by an other user. The designer will show an error message and open the item in read-only mode.
        return true;
    }
    // ...
}
 IRepository.UnlockItem

This is required to unlock an element in the Repository if it was locked with the call of IRepository.LockItem before:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void UnlockItem(string id)
    {
        // ...
    }
    // ...
}
 Helper function to update the meta data of an Repository element

To be able to update meta data of an element in the Repository like the display name in the designer of a project file, it requires a little helper function which writes this adaptations into the SQLite database:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void SetItemMetadata(string itemID, string descriptor)
    {
        _db.CreateCommand(@"
             UPDATE RepoItems
             SET Descriptor = @Descriptor
             WHERE ItemID = @ItemID")
                .SetParameter("Descriptor", descriptor)
                .SetParameter("ItemID", itemID)
                .ExecuteNonQuery();
    }
    // ...
}

From now all concerning List & Label files are managed in a SQLite database. How to work with this Repository is shown in "Step 3: Usage of the self-created IRepository implementation".

 Step 3: Usage of the self-created IRepository implementation
Helper class for the simple access to the Repository

With the help of this class the access to the self implemented Repository is simplified and it is also the base for the following steps:

// Helper class
public class RepositoryHelper
{
    // Implemented interface from Step 2
    private static SQLiteFileRepository _fileRepository;
    public static SQLiteFileRepository GetCurrentRepository()
    {
        if (_fileRepository == null)
        {
            _fileRepository = new SQLiteFileRepository(Global.RepositoryDatabaseFile);
        }
       
        return _fileRepository;
    }
   
    // Sets a display name for a repository item. 
    // Note that this name is only used in the dialogs of the designer!
    // The repository item is still only identified by it's ID.
    public static void SetRepositoryItemProperties(string itemId, string name)
    {
        RepostoryItem modifiedItem = GetCurrentRepository().GetItem(itemId);

        // Get the (encoded) descriptor of this repository item (contains metadata like the display name).
        string descriptorString = modifiedItem.Descriptor;

        // Decode the string and set the entered display name for the UI.
        var descriptor = RepositoryItemDescriptor.LoadFromDescriptorString(descriptorString);
        descriptor.SetUIName(0, name);   // 0 = Standardsprache
        descriptorString = descriptor.SerializeToString();

        // Save the updated descriptor in our repository.
        GetCurrentRepository().SetItemMetadata(itemId, descriptorString);
    }
}

 

Add/Import of existing files from file system into the Repository

In order to be able to import existing files like project files, drawings, shapefiles etc. you can use the supplied helper class RepositoryImportUtil for that:

// Executes a suitable import call for the file type and returns the ID(s) of the created repository item(s).
private void AddFileToRepository(RepositoryItemType fileType, string file1, string file2)
{
    string createdItemId1 = null;
    string createdItemId2 = null;
    // The RepositoryImportUtil class helps to create new items & import existing files.
    using (RepositoryImportUtil util = new RepositoryImportUtil(RepositoryHelper.GetCurrentRepository()))
    {
        using (ListLabel LL = new ListLabel())
        {
            // Consider the possibility to pass a custom information to the CreateOrUpdate() method (in SQLiteFileRepository),
            // which is internally called by the import and where this data will be available in the "userImportData" parameter.
            string userImportData = "Some custom information for your repository";
            if (RepositoryItemType.IsProjectType(fileType))
            {
                createdItemId1 = util.ImportProjectFile(LL, file1, userImportData /* , printerConfigFile, sketchImageFile */);
            }
            else if (fileType == RepositoryItemType.Image)
            {
                createdItemId1 = util.ImportImageFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.PDF)
            {
                createdItemId1 = util.ImportPdfFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.ProjectReverseSide)
            {
                createdItemId1 = util.ImportReverseSideFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.ProjectTableOfContents)
            {
                createdItemId1 = util.ImportTableOfContentsFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.ProjectIndex)
            {
                createdItemId1 = util.ImportIndexFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.Shapefile)
            {
                // ImportShapeFile() returns two IDs (for shapefile and database file)
                var createdShapeFileItems = util.ImportShapefile(LL, file1, file2, userImportData);
                createdItemId1 = createdShapeFileItems.Item1;  
                createdItemId2 = createdShapeFileItems.Item2;
            }
           
            // Use the original file name (without file extension) as the UI display name of the repository item and add it as report to the toolbar if requested.
            string displayName1 = file1.FileName;
            if (fileType != RepositoryItemType.Shapefile)   // Keep file extension only for shapefiles (two files with the same name)
                displayName1 = Path.GetFileNameWithoutExtension(displayName1);
           
            RepositoryHelper.SetRepositoryItemProperties(createdItemId1, displayName1);
           
            if (createdItemId2 != null)
                RepositoryHelper.SetRepositoryItemProperties(createdItemId2, Path.GetFileNameWithoutExtension(file2));
        }
    }
}

 

Creating new elements (new project files) into the Repository

If a new project file should be created and editing in the designer you also can use the helper class RepositoryImportUtil and its suitable function CreateNewProject:

private void CreateNewRepositoryItem(LlProject projectType, string name)
{
    // The RepositoryImportUtil class helps to create new items & import existing files.
    string createdItemID;
    using (RepositoryImportUtil util = new RepositoryImportUtil(RepositoryHelper.GetCurrentRepository()))
    {
        createdItemID = util.CreateNewProject(projectType, name);
    }

    // If we don't want to see the ID of the repository item, we need to set a name to display in the UI.
    RepositoryHelper.SetRepositoryItemProperties(createdItemID, name);
}
       

Delete elements from the Repository

To delete elements from the Repository based on their ID you can directly call the provided implementation of IRepository.DeleteItem:

private void DeleteRepositoryItem(String itemID)
{
    RepositoryHelper.GetCurrentRepository().DeleteItem(itemID);
}

The supplied programming samples show the implementation of the IRepository-Interface based on a SQLite database and its following usage in detail.